(*
NAnt task extension for F# compiler
Copyright (c) 2008 Art Wild

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

Art Wild (dr.d@mail.ru)
*)
#light

namespace NAnt.DotNet.Tasks

#r @"NAnt.Core.dll"
#r @"NAnt.DotNetTasks.dll"
#r @"NDoc.Core.dll"

open System
open System.Globalization
open System.IO
open System.Collections
open System.Collections.Specialized 
open System.Text
open System.Text.RegularExpressions

open NAnt.Core
open NAnt.Core.Attributes
open NAnt.Core.Tasks
open NAnt.Core.Types
open NAnt.Core.Util

open NAnt.DotNet.Types
open NAnt.DotNet.Tasks

open NDoc.Core

/// <summary>
/// Compiles F# programs.
/// </summary>
/// <example>
///   <para>Compile a "HelloWorld" application, including embedded resources.</para>
///   <code>
///     <![CDATA[
/// <fsc target="exe" output="HelloWorld.exe" debug="true">
///     <nowarn>
///         <warning number="62" />
///     </nowarn>
///     <sources>
///         <include name="HelloWorld.fs" />
///     </sources>
///     <references>
///         <include name="System.dll" />
///         <include name="System.Data.dll" />
///     </references>
/// </fsc>
///     ]]>
///   </code>
/// </example>
[<TaskName("fsc")>]
[<ProgramLocation(LocationType.FrameworkDir)>]
type FscTask() = class
    inherit ExternalProgramBase() as base
    
    let mutable _docFile : FileInfo = null 
    let mutable _optimize = false
    let mutable _optimizationlevel = "3"    
    let mutable _warnings = true
        
    let mutable responseFileName = ""
    let mutable _outputFile : FileInfo = null
    let mutable _target = ""
    let mutable _debug = false
    let mutable _define = null
    let mutable _warnAsError = false
    let mutable _warningAsError = new WarningAsError()
    let mutable _noWarn = null
    let mutable _suppressWarnings = new CompilerWarningCollection()
    let mutable _forceRebuild = false
    let mutable _keyFile : FileInfo = null    
    let mutable _references = new AssemblyFileSet()
    let mutable _sources = new FileSet()
    let mutable _resourcesList = new ResourceFileSetCollection()              
           
    /// <summary>
    /// Gets the command-line arguments for the external program.
    /// </summary>
    /// <value>
    /// The command-line arguments for the external program.
    /// </value>
    override x.ProgramArguments = 
        if (PlatformHelper.IsWin32) then 
            "@" + "\"" + responseFileName + "\""    
        else
            let lines = File.ReadAllLines responseFileName  
            Array.fold_left (fun acc line -> acc + " " + line) "" lines

    /// <summary>
    /// Generate debug output. The default is <see langword="false" />.
    /// </summary>
    [<TaskAttribute("debug")>]
    [<BooleanValidator()>]
    member public x.Debug
        with get() = _debug
        and  set(value) = _debug <- value

    /// <summary>
    /// The output file created by the compiler.
    /// </summary>
    [<TaskAttribute("output", Required=true)>]
    member public x.OutputFile
        with get() = _outputFile
        and  set(value) = _outputFile <- value
    
    /// <summary>
    /// Output type. Possible values are <c>exe</c>, <c>winexe</c>, <c>dll</c> or <c>module</c>.
    /// </summary>
    [<TaskAttribute("target", Required=true)>]
    [<StringValidator(AllowEmpty=false)>]
    member public x.OutputTarget
        with get() = _target
        and  set(value) =  _target <- value

    /// <summary>
    /// Define conditional compilation symbol(s).
    /// </summary>
    /// <remarks>
    /// <para>
    /// Corresponds to <c>--d[efine]</c> flag.
    /// </para>
    /// </remarks>
    [<TaskAttribute("define")>]
    member public x.Define
        with get() = _define
        and  set(value) =  _define <- value    
        
    /// <summary>
    /// Instructs the compiler to treat all warnings as errors. The default
    /// is <see langword="false" />.
    /// </summary>
    /// <remarks>
    /// <para>
    /// Corresponds to the <c>--all-warnings-as-errors</c> flag of the compiler.
    /// </para>
    /// <para>
    /// When this property is set to <see langword="true" />, any messages
    /// that would ordinarily be reported as warnings will instead be
    /// reported as errors.
    /// </para>
    /// </remarks>
    [<TaskAttribute("warnaserror")>]
    [<BooleanValidator()>]
    member public x.WarnAsError
        with get() = _warnAsError
        and  set(value) =  _warnAsError <- value              

    /// <summary>
    /// Controls which warnings should be reported as errors.
    /// </summary>
    /// <remarks>    
    /// <para>
    /// Corresponds to the <c>--warn-as-error <int></c> flag of the compiler.
    /// </para>
    /// </remarks>    
    [<BuildElement("warnaserror")>]
    member public x.WarningAsError
        with get() = _warningAsError

    /// <summary>
    /// Specifies the warnings for the compiler to display. The default 
    /// is <see langword="false" />.
    /// </summary>
    /// <value>
    /// The warnings for the compiler to display.
    /// </value>
    /// <remarks>
    /// <para>
    /// Corresponds with the <c>--all-warnings</c>  and <c>--no-warnings</c> flag.
    /// </para>
    /// </remarks>
    [<TaskAttribute("warnings")>]
    [<BooleanValidator()>]   
    member public x.Warnings
        with get() = _warnings
        and  set(value) = _warnings <- value
        
    /// <summary>
    /// Specifies a list of warnings that you want the compiler to suppress.
    /// </summary>
    /// <remarks>
    /// <para>
    /// Corresponds with the <c>--no-warn <string></c> flag.
    /// </para>
    /// </remarks>
    [<BuildElementCollection("nowarn", "warning")>]
    member public x.SuppressWarnings
        with get() = _suppressWarnings

    /// <summary>
    /// Instructs NAnt to recompile the output file regardless of the file timestamps.
    /// </summary>
    /// <remarks>
    /// When this parameter is to <see langword="true" />, NAnt will always
    /// run the compiler to rebuild the output file, regardless of the file timestamps.
    /// </remarks>
    [<TaskAttribute("rebuild")>]
    [<BooleanValidator()>]
    member public x.ForceRebuild
        with get() = _forceRebuild
        and  set(value) =  _forceRebuild <- value     
    
    /// <summary>
    /// Specifies a strong name key file.
    /// </summary>
    [<TaskAttribute("keyfile")>]
    member public x.KeyFile
        with get() = _keyFile
        and  set(value) =  _keyFile <- value  

    /// <summary>
    /// Reference metadata from the specified assembly files.
    /// </summary>
    [<BuildElement("references")>]
    member public x.References
        with get() = _references
        and  set(value) =  _references <- value     

    /// <summary>
    /// Resources to embed.
    /// </summary>
    /// <remarks>
    /// <para>
    /// This can be a combination of resx files and file resources.
    /// </para>
    /// <para>
    /// .resx files will be compiled by <see cref="ResGenTask" /> and then
    /// embedded into the resulting executable.
    /// </para>
    /// <para>
    /// The <see cref="ResourceFileSet.Prefix" /> property is used to make
    /// up the resource name added to the assembly manifest for non-resx
    /// files.
    /// </para>
    /// <para>
    /// For .resx files the namespace from the matching source file is used
    /// as prefix. This matches the behaviour of Visual Studio.
    /// </para>
    /// <para>
    /// Multiple resources tags with different namespace prefixes may be
    /// specified.
    /// </para>
    /// </remarks>
    [<BuildElementArray("resources")>]
    member public x.ResourcesList
        with get() = _resourcesList
                
    /// <summary>
    /// The set of source files for compilation.
    /// </summary>
    [<BuildElement("sources", Required=true)>]
    member public x.Sources
        with get() = _sources
        and  set(value) =  _sources <- value      
                    
    /// <summary>
    /// The name of the XML documentation file to generate.
    /// </summary>
    /// <remarks>
    /// <para>
    /// Corresponds with the <c>-doc string</c> flag.
    /// </para>
    /// </remarks>
    [<TaskAttribute("doc")>]
    member public x.DocFile
        with get() = _docFile
        and  set(value) =  _docFile <- value

    /// <summary>
    /// Specifies whether the compiler should perform optimizations to the 
    /// make output files smaller, faster, and more effecient. The default 
    /// is <see langword="false" />.
    /// </summary>
    /// <value>
    /// <see langword="true" /> if the compiler should perform optimizations; 
    /// otherwise, <see langword="false" />.
    /// </value>
    /// <remarks>
    /// <para>
    /// Corresponds with the <c>-Ooff</c> flag.
    /// </para>
    /// </remarks>
    [<TaskAttribute("optimize")>]
    [<BooleanValidator()>]
    member public x.Optimize
        with get() = _optimize
        and  set(value) =  _optimize <- value


    /// <summary>
    /// Specifies the optimization level for the compiler. Valid values 
    /// are <c>0</c>-<c>3</c>. The default is <c>3</c>.
    /// </summary>
    /// <value>
    /// Specifies the optimization level for the compiler.
    /// </value>
    /// <remarks>
    /// <para>
    /// Corresponds with the <c>-O<int></c> flag.
    /// </para>
    /// </remarks>
    [<TaskAttribute("optimizationlevel")>]
    [<Int32Validator(0, 3)>]
    member public x.OptimizationLevel
        with get() = _optimizationlevel
        and  set(value) = _optimizationlevel <- StringUtils.ConvertEmptyToNull(value)

    /// <summary>
    /// Writes an option using the default output format.
    /// </summary>
    /// <param name="writer">The <see cref="TextWriter" /> to which the compiler options should be written.</param>
    /// <param name="name">The name of the option which should be passed to the compiler.</param>
    member x.WriteOption(writer:TextWriter, name) = 
        writer.WriteLine("-{0}", box(name))    

    /// <summary>
    /// Writes an option and its value using the default output format.
    /// </summary>
    /// <param name="writer">The <see cref="TextWriter" /> to which the compiler options should be written.</param>
    /// <param name="name">The name of the option which should be passed to the compiler.</param>
    /// <param name="value">The value of the option which should be passed to the compiler.</param>
    /// <remarks>
    /// The combination of <paramref name="option" /> and 
    /// <paramref name="value" /> (separated by a colon) is quoted
    /// unless <paramref name="value" /> is already surrounded by quotes.
    /// </remarks>
    member x.WriteOption(writer:TextWriter, name, value:string) = 
        // quote argument if value is not already quoted
        if not (value.StartsWith("\"")) or not (value.EndsWith("\"")) then
            writer.WriteLine("-{0} \"{1}\"", name, value)
        else
            writer.WriteLine("-{0} {1}", name, value)            

    /// <summary>
    /// Determines whether compilation is needed.
    /// </summary>
    member x.NeedsCompiling() =   
        let c = ref false
        if (x.DocFile <> null) then
            if not (x.DocFile.Exists) then
                x.Log(Level.Verbose, "Doc file '{0}' does not exist, recompiling",  [|box(x.DocFile.FullName)|])
                c := true
        
        // return true as soon as we know we need to compile    
        if (x.ForceRebuild) then
            x.Log(Level.Verbose, "'rebuild' attribute set to true, recompiling.")
            c := true

        if not(x.OutputFile.Exists) then
            x.Log(Level.Verbose, "Output file '{0}' does not exist, rebuilding.", [|box(x.OutputFile.FullName)|])
            c := true
        
        // check if sources were updated
        let fileName = FileSet.FindMoreRecentLastWriteTime(x.Sources.FileNames, x.OutputFile.LastWriteTime)
        if (fileName <> null) then
            x.Log(Level.Verbose, "'{0}' has been updated, recompiling.", [|box(fileName)|])
            c := true            
            
        // check if key file was updated
        if (x.KeyFile <> null) then
            let fileName = FileSet.FindMoreRecentLastWriteTime(x.KeyFile.FullName, x.OutputFile.LastWriteTime)
            if (fileName <> null) then
                x.Log(Level.Verbose, "'{0}' has been updated, recompiling.", [|box(fileName)|])
                c := true

        // check if reference assemblies were updated
        let fileName = FileSet.FindMoreRecentLastWriteTime(x.References.FileNames, x.OutputFile.LastWriteTime)
        if (fileName <> null) then
            x.Log(Level.Verbose, "'{0}' has been updated, recompiling.", [|box(fileName)|])
            c := true

        // check if resources were updated
        for resources in x.ResourcesList do
            let fileName = FileSet.FindMoreRecentLastWriteTime(resources.FileNames, x.OutputFile.LastWriteTime)
            if (fileName <> null) then
                x.Log(Level.Verbose, "'{0}' has been updated, recompiling.", [|box(fileName)|])
                c := true
        !c            
            
    /// <summary>
    /// Compiles the sources and resources.
    /// </summary>
    override x.ExecuteTask() =
        if x.NeedsCompiling() then
            // create temp response file to hold compiler options
            responseFileName <- Path.GetTempFileName()
            let writer = new StreamWriter(responseFileName)
            // culture names are not case-sensitive
            let cultureResources = CollectionsUtil.CreateCaseInsensitiveHashtable()
            // will hold temporary compiled resources 
            let compiledResourceFiles = new StringCollection()
            
            try
                // ensure base directory is set, even if fileset was not initialized
                // from XML
                if (x.References.BaseDirectory = null) then
                    x.References.BaseDirectory <- new DirectoryInfo(x.Project.BaseDirectory)                
                
                if (x.Sources.BaseDirectory = null) then 
                    x.Sources.BaseDirectory <- new DirectoryInfo(x.Project.BaseDirectory)                               

                // rescan to ensure correct assembly resolution
                x.References.Scan()
                
                x.Log(Level.Info, "Compiling {0} files to '{1}'.", [|box(x.Sources.FileNames.Count); box(x.OutputFile.FullName)|])                

                // causes the compiler to specify the full path of the file in which 
                // an error was found
                x.WriteOption(writer, "-fullpaths")

                // XML documentation
                if (x.DocFile <> null) then
                    x.WriteOption(writer, "doc", x.DocFile.FullName)

                // generate debuging information
                if (x.Debug) then 
                    x.WriteOption(writer, "g") 
                else
                    if (not x.Optimize) then 
                        x.WriteOption(writer, "Ooff")
                    else
                        x.WriteOption(writer, ("O"+x.OptimizationLevel))
                    
                if (x.Warnings) then
                    x.WriteOption(writer, "-all-warnings")                      
                    // write list of warnings to suppress
                    x.WriteNoWarnList(writer)        
                else
                    x.WriteOption(writer, "-no-warnings")   
                    
                // write warnings to (not) treat as errors to the response file
                x.WriteWarningsAsError(writer)                                                            
                
                // specify output file format
                x.WriteOption(writer, "-target-"+x.OutputTarget)

                x.WriteConditionalCompilationConstants(writer)

                // the name of the output file
                x.WriteOption(writer, "o", x.OutputFile.FullName)

                if (x.KeyFile <> null) then
                    x.WriteOption(writer, "-keyfile", x.KeyFile.FullName)

                // writes assembly references to the response file
                for fileName in x.References.Lib.DirectoryNames do
                    x.WriteOption(writer, "I", fileName)    

                // writes assembly references to the response file
                for fileName in x.References.FileNames do
                    x.WriteOption(writer, "r", fileName)       

                // compile resources
                (*for resources in x.ResourcesList do
                    // resx files
                    if (resources.ResxFiles.FileNames.Count > 0) then
                        // compile the resx files to .resources files in the 
                        // same dir as the input files
                        x.CompileResxResources(resources.ResxFiles.FileNames)

                        // Resx args
                        for fileName in resources.ResxFiles.FileNames do
                            // determine manifest resource name
                            let manifestResourceName = x.GetManifestResourceName(resources, fileName)

                            // determine the filenames of the .resources file
                            // generated by the <resgen> task
                            let tmpResourcePath = Path.ChangeExtension(fileName, ".resources")
                            compiledResourceFiles.Add(tmpResourcePath) |> ignore

                            // check if resource is localized
                            let resourceCulture = CompilerBase.GetResourceCulture(fileName, Path.ChangeExtension(fileName, x.Extension))
                            if (resourceCulture <> null) then
                                if not (cultureResources.ContainsKey(resourceCulture.Name)) then
                                    // initialize collection for holding 
                                    // resource file for this culture
                                    cultureResources.Add(resourceCulture.Name, new Hashtable())
                                    
                                // store resulting .resources file for later linking 
                                ((cultureResources.[resourceCulture.Name]) :?> Hashtable).[manifestResourceName] <- tmpResourcePath
                            else
                                // regular embedded resources (using filename and manifest resource name).
                                let resourceoption = String.Format(CultureInfo.InvariantCulture, "{0},{1}", [|box(tmpResourcePath); box(manifestResourceName)|])
                                // write resource option to response file
                                if resourceoption.Length > 0 then 
                                    x.WriteOption(writer, "-link-resource", resourceoption)*)

                // write sources to compile to response file
                for fileName in x.Sources.FileNames do
                    writer.WriteLine("\"" + fileName + "\"")

                // make sure to close the response file otherwise contents
                // will not be written to disk and ExecuteTask() will fail.
                writer.Close()

                // display response file contents
                if (x.Verbose) then                    
                    x.Log(Level.Info, "Contents of {0}.", [|box(responseFileName)|])                    
                    let reader = File.OpenText(responseFileName)
                    x.Log(Level.Info, reader.ReadToEnd())
                    reader.Close()

                // call base class to do the work
                base.ExecuteTask()
                
            finally
                // cleanup .resource files
                for compiledResourceFile in compiledResourceFiles do
                    File.Delete(compiledResourceFile)                
                
                // make sure we delete response file even if an exception is thrown
                writer.Close() // make sure stream is closed or file cannot be deleted
                File.Delete(responseFileName)   

    /// <summary>
    /// Writes list of warnings to (not) treat as errors to the specified 
    /// <see cref="TextWriter" />.
    /// </summary>
    /// <param name="writer">The <see cref="TextWriter" /> to which the list of warnings should be written.</param>
    member x.WriteWarningsAsError(writer) =
        // check if all warnings should be treated as errors
        if (x.WarnAsError) then
            x.WriteOption(writer, "-all-warnings-as-errors")            
        else
            // warnings that should be treated as error
            for warning in x.WarningAsError.Includes do
                if (warning.IfDefined && not warning.UnlessDefined) then
                    writer.WriteLine("--warn-as-error " + warning.Number)

    /// <summary>
    /// Writes conditional compilation constants to the specified
    /// <see cref="TextWriter" />.
    /// </summary>
    /// <param name="writer">The <see cref="TextWriter" /> to which the conditional compilation constants should be written.</param>
    member x.WriteConditionalCompilationConstants(writer) =
        if (x.Define <> null) then
            writer.WriteLine("--define " + x.Define)

    /// Writes list of warnings to suppress to the specified 
    /// <see cref="TextWriter" />.
    /// </summary>
    /// <param name="writer">The <see cref="TextWriter" /> to which the list of warnings to suppress should be written.</param>
    member x.WriteNoWarnList(writer) =
        for warning in x.SuppressWarnings do
            if (warning.IfDefined && not warning.UnlessDefined) then
                writer.WriteLine("--no-warn " + warning.Number)
                        
    /// <summary>
    /// Gets the file extension required by the current compiler.
    /// </summary>
    /// <value>
    /// For the F# compiler, the file extension is always <c>cs</c>.
    /// </value>
    member x.Extension = "fs"    

end
